package com.gitee.fastmybatis.core;

import com.gitee.fastmybatis.core.ext.MapperLocationsBuilder;
import com.gitee.fastmybatis.core.ext.MyBatisResource;
import com.gitee.fastmybatis.core.ext.spi.ClassSearch;
import com.gitee.fastmybatis.core.ext.spi.SpiContext;
import com.gitee.fastmybatis.core.util.DbUtil;
import com.gitee.fastmybatis.core.util.IOUtil;
import com.gitee.fastmybatis.core.util.StringUtil;
import org.apache.ibatis.builder.xml.XMLMapperBuilder;
import org.apache.ibatis.mapping.Environment;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.apache.ibatis.transaction.TransactionFactory;
import org.apache.ibatis.transaction.jdbc.JdbcTransactionFactory;

import javax.sql.DataSource;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * @author thc
 */
public class MybatisLoader {

    private List<Interceptor> plugins = new ArrayList<>();

    private TransactionFactory transactionFactory = new JdbcTransactionFactory();

    private final DataSource dataSource;

    protected SqlSessionFactoryBuilder sqlSessionFactoryBuilder;

    private final Configuration configuration;

    protected FastmybatisConfig globalConfig;
    protected MapperLocationsBuilder mapperLocationsBuilder;
    protected List<String> mapperResources;

    protected String[] mapperLocations;

    private String dialect;

    protected String basePackage;

    private List<Class<?>> mapperClassList;

    protected List<MyBatisResource> myBatisResources = new ArrayList<>(16);

    private static final AtomicInteger envIndex = new AtomicInteger();

    public MybatisLoader(FastmybatisConfig globalConfig, DataSource dataSource) {
        this(globalConfig, dataSource, new Configuration());
    }

    public MybatisLoader(FastmybatisConfig globalConfig, DataSource dataSource, Configuration configuration) {
        this.globalConfig = globalConfig;
        this.dataSource = dataSource;
        this.configuration = configuration;
    }

    public void load() {
        sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
        Environment.Builder environmentBuilder = new Environment.Builder("env" + envIndex.getAndDecrement())
                .transactionFactory(getTransactionFactory())
                .dataSource(dataSource);
        Configuration conf = getConfiguration();
        conf.setEnvironment(environmentBuilder.build());
        if (this.dialect == null) {
            this.dialect = DbUtil.getDialect(dataSource);
        }
        // init plugin
        initPlugin(conf);

        this.mapperLocationsBuilder = new MapperLocationsBuilder(globalConfig);
        this.mapperResources = new ArrayList<>(8);

        // load xml file
        loadCustomXmlFile();

        try {
            initXml();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    protected void loadCustomXmlFile() {
        if (mapperLocations != null) {
            for (String mapperLocation : mapperLocations) {
                try {
                    List<IOUtil.ResourceFile> resourceFiles = IOUtil.listJarFiles(mapperLocation, ".xml");
                    for (IOUtil.ResourceFile resourceFile : resourceFiles) {
                        myBatisResources.add(MyBatisResource.buildFromFile(resourceFile.getFilename(), resourceFile.getContent()));
                    }
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }
        }
    }

    protected void initPlugin(Configuration conf) {
        if (plugins != null) {
            for (Interceptor plugin : plugins) {
                conf.addInterceptor(plugin);
            }
        }
    }

    protected void initXml() throws IOException {
        Configuration config = getConfiguration();
        if (mapperClassList == null) {
            mapperClassList = new ArrayList<>(10);
        }
        for (Class<?> aClass : mapperClassList) {
            config.addMapper(aClass);
        }
        if (basePackage != null) {
            String[] basePackages = StringUtil.tokenizeToStringArray(basePackage,
                    StringUtil.CONFIG_LOCATION_DELIMITERS);
            ClassSearch classSearch = SpiContext.getClassSearch();
            try {
                Set<Class<?>> clazzsSet = classSearch.search(Object.class, basePackages);
                mapperClassList.addAll(clazzsSet);
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }
        MyBatisResource[] allMybatisMapperResources = mapperLocationsBuilder.build(new HashSet<>(mapperClassList), myBatisResources, dialect);
        for (MyBatisResource myBatisResource : allMybatisMapperResources) {
            try (InputStream inputStream = myBatisResource.getInputStream()) {
                String resource = myBatisResource.getFilepath();
                if (resource == null) {
                    resource = myBatisResource.getFilename();
                }
                XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, config, resource, config.getSqlFragments());
                mapperParser.parse();
            }
        }
    }

    public void setMapperClassList(List<Class<?>> mapperClassList) {
        this.mapperClassList = mapperClassList;
    }

    public TransactionFactory getTransactionFactory() {
        return transactionFactory;
    }

    public void setTransactionFactory(TransactionFactory transactionFactory) {
        this.transactionFactory = transactionFactory;
    }

    public Configuration getConfiguration() {
        return configuration;
    }

    public SqlSessionFactoryBuilder getSqlSessionFactoryBuilder() {
        return sqlSessionFactoryBuilder;
    }

    public void setBasePackage(String basePackage) {
        this.basePackage = basePackage;
    }

    public void setMapperLocations(String[] mapperLocations) {
        this.mapperLocations = mapperLocations;
    }

    public void setPlugins(List<Interceptor> plugins) {
        this.plugins = plugins;
    }

    public MapperLocationsBuilder getMapperLocationsBuilder() {
        return mapperLocationsBuilder;
    }
}
